import datetime
import json
from functools import partial
from math import ceil
from pathlib import PurePath
from typing import List, Callable, Coroutine, Any, Literal, Optional, Tuple

from anyio import create_task_group
from fastapi import BackgroundTasks, Request
from fastapi.responses import JSONResponse

from my_webdav.config import app_setting
from .cache_model import CacheLayer, CacheData
from ..client_115.client import ClientFor115, DirNotFound
from ..client_115.schemas import FileOrFolder, FolderResp, FileResp, DirList

LOG = app_setting.app_log


class ResNotFound(Exception):
    bck_task = None

    def __init__(self, bck_task: BackgroundTasks = None):
        self.bck_task = bck_task


async def notfound_handler(request: Request, exc: ResNotFound):
    _, _ = request, exc
    return JSONResponse(
        status_code=404,
        content={"detail": "ResNotFound"},
        background=exc.bck_task
    )


class DeleteError(Exception):
    pass


class MkdirError(Exception):
    pass


class MoveError(Exception):
    pass


class CopyError(Exception):
    pass


async def task_wrap(
        func: Callable[[Any], Coroutine[Any, Any, DirList]], collect: list, *args
):
    r = await func(*args)
    collect.extend(r.data)


def offset_list(start: int, total: int, page_size: int) -> List[int]:
    return [int(start + i * page_size) for i in range(ceil((total - start) / page_size))]


def create_cache_data(info: FileOrFolder, dir_path: PurePath, cur_time, uid: int):
    name = info.n
    res_id = info.fid if isinstance(info, FileResp) else info.cid
    is_root = False if isinstance(info, FileResp) else info.is_root
    data = CacheData(
        uid=uid,
        dir_path=dir_path.as_posix() if not is_root else None,
        full_path=dir_path.joinpath(name).as_posix() if not is_root else dir_path.as_posix(),
        res_id=res_id,
        res_info=json.dumps(info.dict(), ensure_ascii=False),
        update_time=cur_time
    )
    return data


def search_in_dir(f_list: List[FileOrFolder], name: str) -> Optional[FileOrFolder]:
    match_list = [x for x in f_list if x.n == name]
    if match_list:
        return match_list[0]
    else:
        return None


class OperateLayer(object):
    def __init__(
            self, client: ClientFor115, cache: CacheLayer,
            bck_task: Optional[BackgroundTasks] = None
    ):
        self.client = client
        self.cache = cache
        self.bck_task = bck_task

    async def list_dir_by_client(self, res_id: str, page_size: int = 1024) -> List[FileOrFolder]:
        """
        封装Client的list_dir方法
        :param res_id:
        :param page_size:
        :return:
        """
        members = []
        p1: DirList = await self.client.list_dir(res_id, limit=page_size)
        members.extend(p1.data)
        total_cnt = p1.count

        if total_cnt > page_size:
            # 该目录下文件数超过page_size
            start = page_size
            async with create_task_group() as tg:
                # 创建task_group并发执行请求
                for offset in offset_list(start, total_cnt, page_size):
                    partial_func = partial(
                        self.client.list_dir, cid=res_id, offset=offset, limit=page_size
                    )
                    tg.start_soon(task_wrap, partial_func, members)

        return members

    async def find_in_dir_by_client(self, cid: str, name: str) -> Optional[FileOrFolder]:
        """
        通过Client查询cid文件夹下文件, 找出指定名称的元素.
        Client未开发基于115api中files/search的方法, 因此通过筛选list_dir_by_client的结果返回数据
        :param cid:
        :param name:
        :return:
        """
        try:
            members = await self.list_dir_by_client(cid)
            f_seq = [m for m in members if m.n == name]
            if not f_seq:
                return None
            else:
                return f_seq[0]
        except DirNotFound:
            return None

    async def info_to_cache(self, infos: List[FileOrFolder], dir_path: PurePath):
        """
        将查询出的文件信息写入缓存
        :param infos:
        :param dir_path:
        :return:
        """
        cur_time = datetime.datetime.now()
        uid = int(self.client.user_id)
        datas = [create_cache_data(info, dir_path, cur_time, uid) for info in infos]
        await self.cache.write_cache(datas)

    async def refresh_cache(self, root_id: str, mount_path: PurePath):
        """
        刷新缓存表: 删除现有数据; 写入root_id文件夹及其一级后代的信息到缓存表
        :param root_id:
        :param mount_path:
        :return:
        """
        _ = await self.cache.delete_all()
        members = await self.list_dir_by_client(root_id)
        root_info = FolderResp(n=mount_path.name, is_root=True, cid=root_id)
        members.append(root_info)

        await self.info_to_cache(members, mount_path)

    async def refresh_spec_dir(self, cid: str, dir_path: PurePath):
        """
        刷新指定的目录 当Client未找到目录时删除缓存表中对应的目录
        :param cid:
        :param dir_path:
        :return:
        """
        LOG.debug(f"refresh dir: {dir_path}")
        # 先将目录下的一级子成员删除
        conds = (("dir_path", dir_path.as_posix()),)
        _ = await self.cache.delete_cache(conds)
        try:
            members = await self.list_dir_by_client(cid)
        except DirNotFound:
            # 删除对应目录
            _ = await self.cache.delete_res(dir_path, is_dir=True)
        else:
            # 缓存表写入members
            await self.info_to_cache(members, dir_path)

    async def search_by_path(self, res_path: PurePath, depth: Literal[0, 1]) -> List[FileOrFolder]:
        """
        根据res_path从缓存中查找数据 依据depth字段返回资源自身或自身和内部成员。
        若未从缓存中查到内部成员, 则通过Client获取并更新到缓存(更新缓存的动作添加到BackgroundTasks)
        假定res_path一定存在于缓存中, 否则抛出异常;
        :param res_path:
        :param depth:
        :return:
        """
        members = await self.cache.list_member(res_path, depth)
        if not members:
            raise ResNotFound

        if depth == 0 or isinstance(members[0], FileResp):
            return members

        if depth == 1 and len(members) > 1:
            return members

        # 余下情况: 查询深度为1 但只返回一条记录且该记录为目录类型 需要通过Client查询其内部成员
        spec_res = members[0]
        try:
            children = await self.list_dir_by_client(str(spec_res.cid))
            self.bck_task.add_task(self.info_to_cache, infos=children, dir_path=res_path)
            members.extend(children)
            return members
        except DirNotFound:
            # 更新缓存: 缓存中删除对应目录记录
            self.bck_task.add_task(self.cache.delete_res, res_path=res_path, is_dir=True)
            raise ResNotFound(self.bck_task)

    async def delete_res(self, res_id: int, dir_id: int, res_path: PurePath, is_dir: bool):
        """
        删除对应的资源: 先调用Client删除对应资源, 若成功则从缓存层删除
        :param res_id:
        :param dir_id:
        :param res_path:
        :param is_dir:
        :return:
        """
        r = await self.client.delete_res(str(dir_id), str(res_id))
        if not r.state:
            raise DeleteError(r.error)
        self.bck_task.add_task(self.cache.delete_res, res_path=res_path, is_dir=is_dir)

    async def make_collection(self, path: PurePath):
        parent_path, name = path.parent, path.name
        parent_id = await self.cache.path_to_id(parent_path)
        if parent_id is None:
            raise MkdirError("parent not found")

        r = await self.client.mkdir(str(parent_id), name)
        if not r.state:
            raise MkdirError(r.error)

        # 刷新父目录
        self.bck_task.add_task(
            self.refresh_spec_dir, cid=str(parent_id), dir_path=parent_path
        )

    async def move_resource(
            self, dst_path: PurePath, res_id: str, res_path: PurePath,
            is_dir: bool
    ):
        parent_path = dst_path.parent
        pid = await self.cache.path_to_id(parent_path)
        if pid is None:
            raise MoveError("parent not found")

        if res_path.parent == parent_path:
            # case1: 重命名
            LOG.debug(f"rename res: {res_path} -> {dst_path}")
            new_name = dst_path.name
            r = await self.client.rename(res_id, new_name)
            if not r.state:
                raise MoveError(r.error)
            self.bck_task.add_task(
                self.refresh_spec_dir, cid=str(pid), dir_path=parent_path
            )
        else:
            # case2: 移动
            LOG.debug(f"move res: {res_path} -> {dst_path}")
            r = await self.client.move(str(pid), res_id)
            if not r.state:
                raise MoveError(r.error)
            # 缓存中删除被移动的资源 刷新目标目录
            self.bck_task.add_task(
                self.cache.delete_res, res_path=res_path, is_dir=is_dir
            )

            self.bck_task.add_task(
                self.refresh_spec_dir, cid=str(pid), dir_path=parent_path
            )

    async def copy_resource(self, dst_path: PurePath, res_id: str):
        parent_path = dst_path.parent
        pid = await self.cache.path_to_id(parent_path)
        if pid is None:
            raise CopyError("parent not found")

        r = await self.client.copy(str(pid), res_id)
        if not r.state:
            raise CopyError(r.error)

        self.bck_task.add_task(
            self.refresh_spec_dir, cid=str(pid), dir_path=parent_path
        )

    async def refresh_seq_dirs(self, dirs_and_members: List[Tuple[PurePath, List[FileOrFolder]]]):
        """
        给定一系列目录和其子成员 刷新到缓存中
        :param dirs_and_members:
        :return:
        """
        cur_time = datetime.datetime.now()
        uid = int(self.client.user_id)
        all_cache_data: List[CacheData] = []
        for dir_path, members in dirs_and_members:
            # 缓存中删除该目录下成员
            conds = (("dir_path", dir_path.as_posix()),)
            await self.cache.delete_cache(conds)
            LOG.debug(f"delete {dir_path}'s members in cache")

            # 构建CacheData对象
            all_cache_data.extend(
                [create_cache_data(info, dir_path, cur_time, uid) for info in members]
            )

        await self.cache.write_cache(all_cache_data)

    async def search_until_find(
            self, path: PurePath, depth: Literal[0, 1], root_path: PurePath
    ) -> List[FileOrFolder]:
        """
        返回该path对应的res_model
        :param depth:
        :param path:
        :param root_path:
        :return:
        """
        path_not_found: List[PurePath] = []
        cur_path = path
        cur_member: Optional[FileOrFolder] = None
        to_cache: List[Tuple[PurePath, List[FileOrFolder]]] = []
        path_stop = root_path.parent
        search_time: int = 1  # 查询缓存的次数
        members: List[FileOrFolder] = []

        while cur_path != path_stop:
            members = await self.cache.list_member(cur_path, 1)
            if members:
                LOG.debug(f"found {cur_path} in cache")
                cur_member = members[0]
                break
            else:
                LOG.debug(f"search {cur_path} in cache not found")
                path_not_found.append(cur_path)
                cur_path = cur_path.parent
                search_time += 1

        if cur_path == path_stop:
            # 回退到根路径时若还未找到则抛出异常
            raise ResNotFound()

        parent_path = cur_path
        for path_query in path_not_found[::-1]:
            if not isinstance(cur_member, FolderResp):
                raise ResNotFound()
            cid = cur_member.cid  # 对应于parent_path的cid
            # 通过client查询目录下文件 找出path_query对应的资源
            try:
                LOG.debug(f"list {parent_path} in client")
                sub_members = await self.list_dir_by_client(str(cid))
            except DirNotFound:
                self.bck_task.add_task(
                    self.cache.delete_res, res_path=parent_path, is_dir=True
                )
                raise ResNotFound(self.bck_task)
            else:
                # client查出的结果放入to_cache内
                to_cache.append((parent_path, sub_members))
                LOG.debug(f"add {parent_path}'s members in cache list")
                cur_member = search_in_dir(sub_members, path_query.name)
                if cur_member is None:
                    raise ResNotFound
                LOG.debug(f"found member: {cur_member.n} in {parent_path}")
                parent_path = path_query

        # 若cur_member为文件夹且depth为1 则查询其子成员
        if isinstance(cur_member, FolderResp) and depth == 1:
            if search_time == 1:
                if len(members) == 1:
                    # 缓存直接命中 但未查出子成员
                    sub_members = await self.list_dir_by_client(str(members[0].cid))
                    LOG.debug(f"add {path}'s members in cache list")
                    to_cache.append((path, sub_members))
                    res_members = members + sub_members
                else:
                    # 缓存内有子成员记录 则直接沿用缓存数据
                    LOG.debug(f"found and return {path}'s members in cache")
                    res_members = members
            else:
                # 未命中缓存 通过client查询子成员
                sub_members = await self.list_dir_by_client(str(cur_member.cid))
                LOG.debug(f"add {path}'s members in cache list")
                to_cache.append((path, sub_members))
                res_members = [cur_member] + sub_members
        else:
            # cur_member不为文件夹或查询深度为0 则直接返回[cur_member]
            res_members = [cur_member]

        # 刷新缓存的操作不放在BackgroundTasks里 因为BackgroundTasks在响应结束后才会执行
        # 当GET一个大文件时 响应时间较长, 不能及时更新缓存
        await self.refresh_seq_dirs(to_cache)

        return res_members


if __name__ == '__main__':
    print(offset_list(3, 10, 3))
    # /a/b/c/d
